package main

import (
	"../common"
	//"encoding/json"
	"strings"
	"time"

	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/disk"
	"github.com/shirou/gopsutil/host"
	"github.com/shirou/gopsutil/load"
	"github.com/shirou/gopsutil/mem"
	"github.com/shirou/gopsutil/net"
)

type Collector struct {
	ch                chan common.DataPoint
	metricHead        string
	interval          int
	diskMetricTimes   int
	counter           int
	timeOffset        int64
	stopping          chan struct{}
	stopped           chan struct{}
	timeout           int
	maxRetryCount     int
	netLastValues     map[string]uint64
	cpuLastValues     map[string]float64
	cpucoreLastValues map[string]map[string]float64
	diskLastValues    map[string]uint64
}

func (c *Collector) SendDataPoints(dps []common.DataPoint) {
	for _, dp := range dps {
		c.ch <- dp
	}
}

func (c *Collector) Stop() {
	log.Info("shut down collector")
	close(c.stopping)
	<-c.stopped
}

func (c *Collector) collectAllMetric(metricNames []string) {
	if c.counter > c.diskMetricTimes {
		c.counter = 0
	}
	for _, metricName := range metricNames {
		var err error
		var dps []common.DataPoint
		switch metricName {
		case "UpTimeAndProcs":
			dps, err = c.getUpTimeAndProcs()
		case "Load":
			dps, err = c.getLoad()
		case "Misc":
			dps, err = c.getMisc()
		case "VirtualMemory":
			dps, err = c.getVirtualMemory()
		case "SwapMemory":
			dps, err = c.getSwapMemory()
		case "CPU":
			dps, err = c.getCPU()
		case "CPUcore":
			dps, err = c.getCPUcore()
		case "NetIOCounters":
			dps, err = c.getNetIOCounters()
		case "TcpStatus":
			dps, err = c.getTcpStatus()
		case "DiskUsage":
			if c.counter == 0 {
				dps, err = c.getDiskUsage()
			}
		case "DiskIOCounters":
			if c.counter == 0 {
				dps, err = c.getDiskIOCounters()
			}
		}
		if err != nil {
			log.Errorf("getting metric  %s error is: %v", metricName, err)
		} else {
			log.Debugf("prepared metric  %s size is: %d", metricName, len(dps))
			c.SendDataPoints(dps)
			log.Debugf("sent metric  %s size is: %d", metricName, len(dps))
		}
	}
	c.counter++
}

func (c *Collector) CollectAllMetric(metricNames []string) {
	c.stopping = make(chan struct{})
	c.stopped = make(chan struct{})
	ticker := time.NewTicker(time.Duration(c.interval) * time.Second)
	for {
		select {
		case <-ticker.C:
			log.Infof("collecting metrics %v", metricNames)
			c.collectAllMetric(metricNames)
		case <-c.stopping:
			ticker.Stop()
			//close(c.ch)
			close(c.stopped)
			log.Info("closed c.stopped, sender is closed")
			return
		}
	}
}

func map2dp(metricHead string, data map[string]float64, delta int64) ([]common.DataPoint, error) {
	dps := []common.DataPoint{}
	t := uint32(common.GetTime(delta))
	for name, value := range data {
		m := metricHead + "." + name
		dps = append(dps, common.DataPoint{m, value, t})
	}
	return dps, nil
}

func (c *Collector) getUpTimeAndProcs() ([]common.DataPoint, error) {
	info, err := host.Info()
	if err != nil {
		return nil, err
	}
	uptime := float64(info.Uptime)
	procs := float64(info.Procs)
	t := uint32(common.GetTime(c.timeOffset))
	dps := []common.DataPoint{
		common.DataPoint{c.metricHead + ".host.uptime", uptime, t},
		common.DataPoint{c.metricHead + ".host.procs", procs, t},
	}
	return dps, nil
}

func (c *Collector) getLoad() ([]common.DataPoint, error) {
	la, err := load.Avg()
	if err != nil {
		return nil, err
	}
	t := uint32(common.GetTime(c.timeOffset))
	dps := []common.DataPoint{
		common.DataPoint{c.metricHead + ".load.load1", float64(la.Load1), t},
		common.DataPoint{c.metricHead + ".load.load5", float64(la.Load5), t},
		common.DataPoint{c.metricHead + ".load.load15", float64(la.Load15), t},
	}
	return dps, nil
}

func (c *Collector) getMisc() ([]common.DataPoint, error) {
	lm, err := load.Misc()
	if err != nil {
		return nil, err
	}
	t := uint32(common.GetTime(c.timeOffset))
	dps := []common.DataPoint{
		common.DataPoint{c.metricHead + ".Misc.procsRunning", float64(lm.ProcsRunning), t},
		common.DataPoint{c.metricHead + ".Misc.procsBlocked", float64(lm.ProcsBlocked), t},
		common.DataPoint{c.metricHead + ".Misc.ctxt", float64(lm.Ctxt), t},
	}
	return dps, nil
}

func (c *Collector) getVirtualMemory() ([]common.DataPoint, error) {
	v, err := mem.VirtualMemory()
	if err != nil {
		return nil, err
	}
	ret := make(map[string]float64)
	if v.Total != 0 {
		ret["used"] = float64(v.Used) / float64(v.Total)
		ret["available"] = float64(v.Available) / float64(v.Total)
		ret["free"] = float64(v.Free) / float64(v.Total)
		ret["active"] = float64(v.Active) / float64(v.Total)
		ret["inactive"] = float64(v.Inactive) / float64(v.Total)
		ret["buffers"] = float64(v.Buffers) / float64(v.Total)
		ret["cached"] = float64(v.Cached) / float64(v.Total)
		ret["shared"] = float64(v.Shared) / float64(v.Total)
	}
	return map2dp(c.metricHead+".mem.percent", ret, c.timeOffset)
}

func (c *Collector) getSwapMemory() ([]common.DataPoint, error) {
	v, err := mem.SwapMemory()
	if err != nil {
		return nil, err
	}
	ret := make(map[string]float64)
	if v.Total != 0 {
		ret["swap_used"] = float64(v.Used) / float64(v.Total)
		ret["swap_free"] = float64(v.Free) / float64(v.Total)
		ret["swap_sin"] = float64(v.Sin) / float64(v.Total)
		ret["swap_sout"] = float64(v.Sout) / float64(v.Total)
	}
	return map2dp(c.metricHead+".mem.percent", ret, c.timeOffset)
}

func (c *Collector) getCPU() ([]common.DataPoint, error) {
	v, err := cpu.Times(false)
	if err != nil || len(v) != 1 {
		return nil, err
	}
	cpuNowValues := make(map[string]float64)
	cpuNowValues["user"] = v[0].User
	cpuNowValues["system"] = v[0].System
	cpuNowValues["idle"] = v[0].Idle
	cpuNowValues["iowait"] = v[0].Iowait
	cpuNowValues["irq"] = v[0].Irq
	cpuNowValues["nice"] = v[0].Nice
	cpuNowValues["softirq"] = v[0].Softirq
	cpuNowValues["steal"] = v[0].Steal
	cpuNowValues["guest"] = v[0].Guest
	cpuNowValues["guestNice"] = v[0].GuestNice
	cpuNowValues["stolen"] = v[0].Stolen
	cpuNowValues["total"] = v[0].User + v[0].System + v[0].Nice + v[0].Iowait + v[0].Irq + v[0].Softirq + v[0].Steal + v[0].Guest + v[0].GuestNice + v[0].Stolen + v[0].Idle
	ret := PercentMinus(c.cpuLastValues, cpuNowValues)
	c.cpuLastValues = cpuNowValues
	return map2dp(c.metricHead+".cpu.cpu_times", ret, c.timeOffset)
}

func (c *Collector) getCPUcore() ([]common.DataPoint, error) {
	var dps []common.DataPoint
	cpuv, err := cpu.Times(true)
	if err != nil || len(cpuv) < 1 {
		return nil, err
	}
	for _, v := range cpuv {
		cpuNowValues := make(map[string]float64)
		name := v.CPU
		cpuNowValues["user"] = v.User
		cpuNowValues["system"] = v.System
		cpuNowValues["idle"] = v.Idle
		cpuNowValues["iowait"] = v.Iowait
		cpuNowValues["irq"] = v.Irq
		cpuNowValues["nice"] = v.Nice
		cpuNowValues["softirq"] = v.Softirq
		cpuNowValues["steal"] = v.Steal
		cpuNowValues["guest"] = v.Guest
		cpuNowValues["guestNice"] = v.GuestNice
		cpuNowValues["stolen"] = v.Stolen
		cpuNowValues["total"] = v.User + v.System + v.Nice + v.Iowait + v.Irq + v.Softirq + v.Steal + v.Guest + v.GuestNice + v.Stolen + v.Idle
		if c.cpucoreLastValues == nil {
			c.cpucoreLastValues = map[string]map[string]float64{}
		}
		ret := PercentMinus(c.cpucoreLastValues[name], cpuNowValues)
		c.cpucoreLastValues[name] = cpuNowValues
		dp, err := map2dp(c.metricHead+".cpu.core."+name, ret, c.timeOffset)
		if err != nil {
			break
		}
		dps = append(dps, dp...)
	}
	return dps, err
}

func PercentMinus(last, now map[string]float64) map[string]float64 {
	lt, ok1 := last["total"]
	nt, ok2 := now["total"]
	ret := make(map[string]float64)
	if ok1 && ok2 {
		total := float64(nt - lt)
		for k, v := range now {
			if lastv, ok := last[k]; ok {
				ret[k] = float64(v-lastv) / total
			}
		}
	}
	return ret
}

func CounterMinus(last, now map[string]uint64) map[string]float64 {
	ret := make(map[string]float64)
	if last == nil {
		return ret
	}
	for k, v := range now {
		lastV, ok := last[k]
		if ok {
			ret[k] = float64(v - lastV)
		}
	}
	return ret
}

func (c *Collector) getNetIOCounters() ([]common.DataPoint, error) {
	ncs, err := net.IOCounters(false)
	if err != nil {
		return nil, err
	}
	netNowValues := make(map[string]uint64)
	for _, nc := range ncs {
		name := nc.Name
		netNowValues[name+".bytes.sent"] = nc.BytesSent
		netNowValues[name+".bytes.recv"] = nc.BytesRecv
		netNowValues[name+".packets.sent"] = nc.PacketsSent
		netNowValues[name+".packets.recv"] = nc.PacketsRecv
	}
	ret := CounterMinus(c.netLastValues, netNowValues)
	c.netLastValues = netNowValues
	return map2dp(c.metricHead+".net.iorate", ret, c.timeOffset)
}

func (c *Collector) getDiskIOCounters() ([]common.DataPoint, error) {
	diskNowValues := make(map[string]uint64)
	dcs, err := disk.IOCounters()
	if err != nil {
		return nil, err
	}
	for name, dc := range dcs {
		name = common.Encode_base64(name)
		diskNowValues[name+".count.read"] = dc.ReadCount
		diskNowValues[name+".count.write"] = dc.WriteCount
		diskNowValues[name+".bytes.read"] = dc.ReadBytes
		diskNowValues[name+".bytes.write"] = dc.WriteBytes
		diskNowValues[name+".iotime"] = dc.IoTime
		diskNowValues[name+".weightedio"] = dc.WeightedIO
	}
	ret := CounterMinus(c.diskLastValues, diskNowValues)
	c.diskLastValues = diskNowValues
	return map2dp(c.metricHead+".disk.iorate", ret, c.timeOffset)
}

func (c *Collector) getDiskUsage() ([]common.DataPoint, error) {
	d, err := disk.Partitions(false)
	if err != nil {
		return nil, err
	}
	ret := make(map[string]float64)
	for _, v := range d {
		u, err := disk.Usage(v.Mountpoint)
		if err != nil {
			return nil, err
		}
		path := common.Encode_base64(v.Mountpoint)
		ret[path+".space"] = u.UsedPercent
		ret[path+".inode"] = u.InodesUsedPercent
	}
	return map2dp(c.metricHead+".disk.percent", ret, c.timeOffset)
}

var TCPSTATUS = map[string]string{
	"01": "established",
	"02": "syn_sent",
	"03": "syn_recv",
	"04": "fin_wait1",
	"05": "fin_wait2",
	"06": "time_wait",
	"07": "close",
	"08": "close_wait",
	"09": "last_ack",
	"0A": "listen",
	"0B": "closing",    /* Now a valid state */
	"0C": "max_states", /* Leave at the end! */
}

func (c *Collector) getTcpStatus() ([]common.DataPoint, error) {
	b, err := ReadTcpFile()
	if err != nil {
		return nil, err
	}
	tcpCount := ParseTcpCount(b)
	return map2dp(c.metricHead+".tcp", tcpCount, c.timeOffset)
}

func ParseTcpCount(b []byte) map[string]float64 {
	lines := strings.Split(string(b), "\n")
	//p := regexp.MustCompile("\\s+")
	ret := make(map[string]float64)
	for _, l := range lines {
		fields := strings.Fields(l)
		if len(fields) < 6 {
			continue
		}
		stat, ok := TCPSTATUS[fields[3]]
		if !ok {
			continue
		}
		ret[stat] = ret[stat] + 1
	}
	return ret
}
